using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Reflection;
using System.Diagnostics;

///
/// Low-level inspector code
/// 

namespace Meta {
	//
	// IInspectable is an interface you can provide to customize the display
	// of your class within the inspector.
	//
	public interface IInspectable {
		string InspectorName();
		IList RenderForInspection();
	}

	//
	// InspectorField is the internal representation used for a line on the
	// display of the inspector.
	//
	[Serializable]
	public class InspectorField {
		protected InspectorField() {
		}

		public InspectorField(string name) {
			fieldName = name;
			hasValue=false;
		}
		public InspectorField(string name, object val) {
			fieldName = name;
			valueString = (val is string)?("\""+(string)val+"\""):Writer.SafeWriteToString(val, InspectorBox.MaxStringLength);
			fieldValue = val;
			hasValue = true;
		}
		public InspectorField(string name, object val, string valueString) {
			fieldName = name;
			this.valueString = valueString;
			fieldValue = val;
			hasValue = true;
		}
		public InspectorField(string name, object val, string valueString, bool forceBold) : this(name, val, valueString, forceBold, true) {
		}

		public InspectorField(string name, object val, string valueString, bool forceBold, bool hasValue) : this(name, val, valueString) {
			this.forceBold = forceBold;
			this.hasValue = hasValue;
		}

		public string fieldName;
		public string valueString;
		public object fieldValue;
		public bool forceBold;
		public bool overWidth=false;
		public bool hasValue;
		public virtual float HeightPad {
			get {
				return 0;
			}
		}
	}

	public abstract class InspectorButton : InspectorField {
		public abstract void Click();
	}

	public class Separator :InspectorButton {
		public override void Click() {
			// Does nothing
			return;
		}
		public Separator(string name) {
			fieldName = name;
			valueString = "";
		}

		public override float HeightPad {
			get {
				return 0.4F;
			}
		}
	}

	public class HelpLink : InspectorButton {
		public override void Click() {
			new HelpBrowser((string)fieldValue).Show();
		}

		public HelpLink(string url) {
			fieldName = "Help";
			valueString = "";
			fieldValue = url;
		}
	}

	//
	// InspectorBox implements the real functionality of the inspector.
	//
	public class InspectorBox : System.Windows.Forms.ListBox {
		public delegate void CurrentObjectChangedHandler(object sender, object newObject);

		public event CurrentObjectChangedHandler CurrentObjectChanged;

		private Brush brush;
		private float valueColumnStart = 100;
		public static Font fieldNameFont;
		public static Font separatorFont;
		public static Font defaultFont;
		private int colorStringWidth=0;
		private int brushStringWidth=0;
		private int penStringWidth=0;
		private ToolTip tooltip;
		public int MaxListElements = 1000;
		public static int MaxStringLength = 200;

		public bool IsDebugger {
			get {
				Inspector inspector = FindForm() as Inspector;

				return inspector!=null && inspector.IsDebugger;
			}
		}

		public InspectorBox() : base() {
			DisplayMember = "DisplayString";
			DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
			brush = new SolidBrush(ForeColor);
			tooltip = new ToolTip();
			tooltip.ShowAlways = true;
		}

		private ArrayList history = new ArrayList();

		class HistoryRecord {
			public object currentObject;
			public int selectionIndex;

			public HistoryRecord(object currentObject, int selectionIndex) {
				this.currentObject = currentObject;
				this.selectionIndex = selectionIndex;
			}
		}

		private int historyIndex = -1;

		public bool CanGoBack {
			get {
				return CanPop;
			}
		}

		public bool CanGoForward {
			get {
				return (historyIndex+1)<history.Count;
			}
		}

		public bool CanExplain {
			get {
#if OLD_EXPLAIN
				return CurrentObject is Exception || (CurrentObject is EvalStack && ((InspectorField)Items[0]).fieldValue is Exception);
#else
				return CurrentObject is EvalStack;
#endif
			}
		}

		public void Push(object obj) {
//			if (history.Count==0 || obj != CurrentObject) {
			// Save current selection
			if (historyIndex>=0)
				((HistoryRecord)history[historyIndex]).selectionIndex = SelectedIndex;
			historyIndex++;
			history.RemoveRange(historyIndex, history.Count-historyIndex);
			history.Add(new HistoryRecord(obj, -1));
			//history.Push(obj);
			ShowNewObject();
//			}
		}

		public void Restart(object obj) {
			historyIndex = -1;
			Push(obj);
			ShowNewObject();
		}

		//  Refresh just repaints the screen w/o recomputing the slots of the object.
		public void UpdateRendering() {
			ShowNewObject();
		}

		public bool CanPop {
			get {
				return historyIndex>0;
			}
		}

		public void Pop() {
			if (CanPop)
				historyIndex--;
			ShowNewObject();
		}

		public void Forward() {
			if (CanGoForward) {
				historyIndex++;
				ShowNewObject();
			}
		}

		public object CurrentObject {
			get {
				Trace.Assert(history.Count>0, "History list is empty");
				return ((HistoryRecord)history[historyIndex]).currentObject;
			}
			set {
				Push(value);
			}
		}

		void ShowNewObject() {
			object currentObject = CurrentObject;

			Namespace.RootNamespace.Define("<>", currentObject);

			BeginUpdate();
			Items.Clear();
			
			//Font = defaultFont;
			if (Utilities.IsRemote(currentObject)) {
				Parent.Text = "Remote object";
				Items.Add(new InspectorField("Type", currentObject.GetType()));
				Items.Add(new InspectorField("URI", System.Runtime.Remoting.RemotingServices.GetObjectUri((MarshalByRefObject)currentObject)));
			} else if (currentObject != null) {
				IInspectable insp = currentObject as IInspectable;

				if (insp != null) {
					Parent.Text = insp.InspectorName();
					foreach (object o in insp.RenderForInspection())
						Items.Add(o);
				} else {
					Type t = currentObject.GetType();
					bool isException = currentObject is Exception;

					if (currentObject is IArglist)
						Items.Add(new InspectorField("Arguments", ((IArglist)currentObject).Arglist));
					if (currentObject is IDocumentable) {
						object doc = ((IDocumentable)currentObject).Documentation;
						if (doc!=null)
							Items.Add(new InspectorField("Arguments", doc));
					}

					Parent.Text = Writer.SafeWriteToString(currentObject, MaxStringLength);
					Items.Add(new InspectorField("Type", t));

					if (currentObject is MethodBase)
						Items.Add(new InspectorField("Parameters", ((MethodBase)currentObject).GetParameters()));

					if (currentObject is Type) {
						bool gotStatic=false;
						Type thetype=(Type)currentObject;

						Items.Add(new InspectorField("Parent type", thetype.BaseType));
						Items.Add(new InspectorField("Interfaces", thetype.GetInterfaces()));
						MemberInfo[] members = thetype.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
						ArrayList methods = new ArrayList();
						ArrayList fields = new ArrayList();
						ArrayList properties = new ArrayList();
						ArrayList events = new ArrayList();
						int startOfMembers=Items.Count;
						if (members!=null) {
							Array.Sort(members,MemberComparer.Default);
							foreach (MemberInfo m in members) {
								switch (m.MemberType) {
									case MemberTypes.Field:
										FieldInfo f = (FieldInfo)m;

										if (f.IsStatic) {
											if (!gotStatic) {
												Items.Add(new Separator("Static fields and properties"));
												gotStatic=true;
											}
											Items.Add(new InspectorField(f.Name, f.GetValue(null)));
										} else
											fields.Add(m);
										break;

									case MemberTypes.Property:
										PropertyInfo p = (PropertyInfo)m;
										if (p.CanRead) {
											MethodInfo getter = p.GetGetMethod(true);
											if (getter.IsStatic && getter.GetParameters().Length==0) {
												if (!gotStatic) {
													Items.Add(new Separator("Static fields and properties"));
													gotStatic=true;
												}
												Items.Add(new InspectorField(p.Name, getter.Invoke(null, new object[0])));
											} else
												properties.Add(m);
										}
										break;

									case MemberTypes.Method:
										if (!(((MethodInfo)m).IsSpecialName))
											methods.Add(m);
										break;

									case MemberTypes.Constructor:
										methods.Add(m);
										break;

									case MemberTypes.Event:
										events.Add(m);
										break;

									default:
										break;
								}
							}
							if (events.Count>0)
								Items.Insert(startOfMembers, new InspectorField("Events", events));
							if (properties.Count>0)
								Items.Insert(startOfMembers, new InspectorField("Properties", properties));
							if (fields.Count>0)
								Items.Insert(startOfMembers, new InspectorField("Fields", fields));
							if (methods.Count>0)
								Items.Insert(startOfMembers, new InspectorField("Methods", methods));
						}
					}

					if (currentObject is IList) {
						IList a = (IList)currentObject;

						for (int i=0; i<Math.Min(a.Count,MaxListElements); i++)
							Items.Add(new InspectorField(i.ToString(), a[i]));
					} else if (currentObject is IDictionary) {
						Items.Add(new Separator("Entries:"));
						IDictionary d = (IDictionary)currentObject;

						foreach (DictionaryEntry e in d)
							Items.Add(new InspectorField(e.Key.ToString(), e.Value));
					} else {
						PropertyInfo[] properties = t.GetProperties(BindingFlags.Public | (Inspector.showDetail?BindingFlags.NonPublic:0)
							| BindingFlags.Instance | (Inspector.showDetail?BindingFlags.Static:0));
						if (properties.Length>0) {
							Array.Sort(properties, MemberComparer.Default);
//							Items.Add(new Separator(""));
							Items.Add(new Separator("Properties:"));
								foreach (PropertyInfo p in properties) {
									ParameterInfo[] param = p.GetIndexParameters();
									if (param.Length==0) {
										try {
											object val = p.GetValue(currentObject, null);

											// Special processing for exceptions
											if (isException) {
												if (p.Name == "HelpLink" && val == null) {
													string myHelpFile = Installation.HelpFilePath("Exception", t.Name);
													if (System.IO.File.Exists(myHelpFile))
														val = myHelpFile;
													if (val != null)
														Items.Insert(0, new HelpLink((string)val));
												} else if (Inspector.showDetail || (IsDebugger || (p.Name != "StackTrace" && (p.Name != "InnerException" || val!=null) && p.Name != "TargetSite" && p.Name != "Source")))
//												} else if (IsDebugger || (p.Name != "StackTrace" && p.Name != "TargetSite" && p.Name != "Source"))
													if (IsDebugger || val !=null || (p.Name != "ParamName"))
														Items.Add(new InspectorField(p.Name, val));
											} else
												Items.Add(new InspectorField(p.Name, val));
										} catch (Exception e) {
											e=e;
											Items.Add(new InspectorField(p.Name, "<Couldn't get value>"));
										}
									}
								}
						}
						FieldInfo[] fields = t.GetFields(BindingFlags.Public | (Inspector.showDetail?BindingFlags.NonPublic:0)
							| BindingFlags.Instance | (Inspector.showDetail?BindingFlags.Static:0));
						if (fields.Length>0) {
							Array.Sort(fields, MemberComparer.Default);
//							Items.Add(new Separator(""));
							Items.Add(new Separator("Fields:"));
							foreach (FieldInfo f in fields)
								Items.Add(new InspectorField(f.Name, f.GetValue(currentObject)));
						}
					}
				}
			} else
				Parent.Text = Writer.SafeWriteToString(currentObject, MaxStringLength);
			
			// Determine column layout
			if (fieldNameFont == null)
				fieldNameFont = Font; //new System.Drawing.Font("Arial Narrow", Font.Size, FontStyle.Bold);
			if (separatorFont == null)
				separatorFont = new System.Drawing.Font(Font, FontStyle.Bold);
			valueColumnStart = 0;
			Graphics g = CreateGraphics();
			foreach (InspectorField i in Items) {
				if (!(i is InspectorButton) && i.hasValue && i.fieldName!=null) {
					float width = g.MeasureString(i.fieldName, fieldNameFont).Width;
					if (width>valueColumnStart)
						valueColumnStart = width;
				}
			}
			valueColumnStart += 10;
			g.Dispose();

			// Kluge so that we can display images in the inspector.
			if (currentObject is Image || currentObject is string)
				Items.Add(currentObject);
			else if (currentObject==null)
				Items.Add("Null is the value used to represent \"nothing\" or the result of an expression that \"has no value\".");
			else if (currentObject is bool) {
				if ((bool)currentObject)
					Items.Add("True is the value used to represent that the statement or question represented by an expression is true.");
				else
					Items.Add("False is the value used to represent that the statement or question represented by an expression is false.");
			}

			int newIndex = ((HistoryRecord)history[historyIndex]).selectionIndex;
			// Need this check because the number of items in an element of the history record can change
			// (e.g. the log can be in the history and can be cleared).
			SelectedIndex = (newIndex<Items.Count)?newIndex:0;

			if (CurrentObjectChanged != null)
				CurrentObjectChanged(this, currentObject);

			EndUpdate();
		}

		protected override void OnMeasureItem(MeasureItemEventArgs e) {
			if (DesignMode) {
				e.ItemHeight = 16;
				e.ItemWidth = 100;
			} else {
				Graphics g = e.Graphics;
				object item = Items[e.Index];

				if (item is InspectorField) {
					InspectorField field = (InspectorField)item;

					if (field.fieldValue is Image) {
						GraphicsUnit u = g.PageUnit;
						RectangleF r = ((Image)field.fieldValue).GetBounds(ref u);
						e.ItemHeight = Math.Max((int)r.Height, 16);
						e.ItemWidth =  (int)Math.Max(r.Width+valueColumnStart, valueColumnStart);
					} else {					
						SizeF s = g.MeasureString(field.hasValue?field.valueString:"", Font);
						e.ItemHeight = (int)(Math.Max(s.Height,16F)*(1+field.HeightPad));
						e.ItemWidth = (int)Math.Max(s.Width+valueColumnStart, valueColumnStart);
					}
					field.overWidth = (e.ItemWidth+valueColumnStart)>e.Graphics.VisibleClipBounds.Width;
				} else if (item is Image) {
					GraphicsUnit u = g.PageUnit;
					RectangleF r = ((Image)item).GetBounds(ref u);
					e.ItemHeight = Math.Max((int)r.Height, 16);
					e.ItemWidth =  (int)Math.Max(r.Width+valueColumnStart, valueColumnStart);
				} else if (item is string) {
					SizeF s = g.MeasureString((string)item, Font);
					e.ItemHeight = Math.Max((int)s.Height, 16);
					e.ItemWidth = (int)s.Width;
				}
			}
		}

		protected override void OnDrawItem(DrawItemEventArgs e) {
			if (DesignMode) {
				e.DrawBackground();
				return;
			}
			if (e.Index>=0) {  // I don't get it, but sometimes this gets called with an index of -1.
				Graphics g = e.Graphics;
				object item = Items[e.Index];

				if (item is Image)
					g.DrawImage((Image)item, new Point(0, e.Bounds.Top));
				else if (item is string)
					g.DrawString((string)item, Font, brush, 0.0F, e.Bounds.Top);
				else {
					InspectorField f = (InspectorField)Items[e.Index];
					object realItem = f.fieldValue;

					e.DrawBackground();
					if (f.fieldName!=null)
						g.DrawString(f.fieldName, f is Separator?separatorFont:fieldNameFont, brush, 0.0F, Math.Max(e.Bounds.Top, e.Bounds.Top+e.Bounds.Height*f.HeightPad-2));
					if (realItem is Image) {
						Image i = (Image)realItem;
						GraphicsUnit u = g.PageUnit;

						RectangleF r = i.GetBounds(ref u);
						float iHeight = r.Height;
						float scale = e.Bounds.Height/iHeight;

						g.DrawImage((Image)f.fieldValue, f.fieldName==null?0:(int)valueColumnStart, (float)e.Bounds.Top, scale*r.Width, scale*r.Height);
					} else if (realItem is Color) {
						g.DrawString("Color", e.Font, brush, valueColumnStart, e.Bounds.Top);

						if (colorStringWidth==0)
							colorStringWidth=(int)g.MeasureString("Color ", Font).Width;

						g.FillRectangle(new SolidBrush((Color)realItem), (int)valueColumnStart+colorStringWidth, e.Bounds.Top, 50, e.Bounds.Height);
					} else if (realItem is Brush) {
						g.DrawString("Brush", e.Font, brush, valueColumnStart, e.Bounds.Top);

						if (brushStringWidth==0)
							brushStringWidth=(int)g.MeasureString("Brush ", Font).Width;

						g.FillRectangle((Brush)realItem, (int)valueColumnStart+brushStringWidth, e.Bounds.Top, 50, e.Bounds.Height);
					} else if (realItem is Pen) {
						int height = e.Bounds.Top+(e.Bounds.Height/2);

						g.DrawString("Pen", e.Font, brush, valueColumnStart, e.Bounds.Top);

						if (penStringWidth==0)
							penStringWidth=(int)g.MeasureString("Pen ", Font).Width;

						g.DrawLine((Pen)realItem, (int)valueColumnStart+penStringWidth, height, (int)valueColumnStart+penStringWidth+50, height);
					} else if (f.hasValue)
						g.DrawString((f.valueString.Length>200)?(f.valueString.Substring(0,200)):f.valueString, f.forceBold?separatorFont:e.Font, brush, f.fieldName==null?20:(int)valueColumnStart, e.Bounds.Top);
					e.DrawFocusRectangle();
				}
			}
			base.OnDrawItem(e);
		}

		protected override void OnMouseMove(MouseEventArgs e) {
			int index=IndexFromPoint(e.X, e.Y);
			if (index==ListBox.NoMatches || index >= Items.Count)
				tooltip.SetToolTip(this, "");
			else {
				object i = Items[index];

				if (i is String)
					tooltip.SetToolTip(this, (string)i);
				else if (i is InspectorField) {
					InspectorField f=(InspectorField)i;
					if (f.fieldValue is Exception)
						tooltip.SetToolTip(this, ((Exception)f.fieldValue).Message);
					else
						tooltip.SetToolTip(this, (!f.overWidth || f is Separator)?"":f.valueString);
				} else
					tooltip.SetToolTip(this, "");
			}
			base.OnMouseMove (e);
		}

		protected override void OnClick(EventArgs e) {
			base.OnClick(e);

			InspectorField item = SelectedItem as InspectorField;
			if (item==null)
				return;
			else if (item is InspectorButton)
				((InspectorButton)item).Click();
			else if (item.hasValue)
				Push(item.fieldValue);
		}

		protected override void OnKeyPress(KeyPressEventArgs e) {
			char key = e.KeyChar;

			if (Char.IsLetter(key)) {
				for (int i = SelectedIndex+1; i<Items.Count; i++) {
					InspectorField o = Items[i] as InspectorField;
					if (o != null && o.fieldName[0]==key) {
						SelectedIndex = i;
						e.Handled = true;
						break;
					}
				}
			} else if (Char.IsDigit(key)) {
				int newIndex = (int)(key-'0');
				if (newIndex<Items.Count) {
					SelectedIndex = newIndex;
					e.Handled = true;
				}
			}

			base.OnKeyPress(e);
		}

		protected override void OnKeyUp(KeyEventArgs e) {
			switch (e.KeyCode) {
				case Keys.Back:
				case Keys.BrowserBack:
					Pop();
					e.Handled = true;
					break;

				case Keys.Escape:
					FindForm().Close();
					break;

				case Keys.Enter:
					InspectorField item = SelectedItem as InspectorField;
					if (item!=null)
						Push(item.fieldValue);
					break;					

				default:
					base.OnKeyUp(e);
					break;
			}
		}

		//
		// Provides an ordering of types under which parents go before children, but types
		// are otherwise ordered by Name.
		//
		public class TypeComparer : IComparer {
			public static readonly IComparer Default = new TypeComparer();

			int System.Collections.IComparer.Compare(object a, object b) {
				Type aType = a as Type;
				Type bType = b as Type;

				if (aType == null)
					return -1;
				if (bType==null)
					return 1;

				if (aType.IsSubclassOf(bType))
					return 1;
				if (bType.IsSubclassOf(aType))
					return -1;

				// Force an ordering based on name
				return aType.Name.CompareTo(bType.Name);
			}
		}

		public class MemberComparer : IComparer {
			public static readonly IComparer Default = new MemberComparer();
			int System.Collections.IComparer.Compare(object a, object b) {
				MemberInfo aInfo = a as MemberInfo;
				MemberInfo bInfo = b as MemberInfo;

				if (aInfo == null)
					return -1;
				if (bInfo == null)
					return 1;

				int nameComparison = aInfo.Name.CompareTo(bInfo.Name);
				if (nameComparison!=0)
					return nameComparison;
				// Names are the same

				MethodInfo aMeth = aInfo as MethodInfo;
				MethodInfo bMeth = bInfo as MethodInfo;

				if (aMeth==null || bMeth==null)
					// We're not comparing methods, so the names are all that matters, and they're the same
					return 0;

				int returnTypeOrdering = TypeComparer.Default.Compare(aMeth.ReturnType, bMeth.ReturnType);
				if (returnTypeOrdering!=0)
					return returnTypeOrdering;

				ParameterInfo[] aParams = aMeth.GetParameters();
				ParameterInfo[] bParams = bMeth.GetParameters();
				if (aParams.Length<bParams.Length)
					return -1;
				if (bParams.Length<aParams.Length)
					return 1;
				for (int i=0; i<aParams.Length; i++) {
					int paramOrdering = TypeComparer.Default.Compare(aParams[i].ParameterType, bParams[i].ParameterType);
					if (paramOrdering!=0)
						return paramOrdering;
				}
				return 0;
			}
		}

		public void ReallyExplain() {
//			try {
				Package diagnostics = TopLevel.thePackagesDirectory.Lookup(Symbol.Intern("Diagnostics"), false, false, true).value as Package;
				Procedure diagnose = diagnostics.Lookup(Symbol.Intern("diagnose"), false, false, true).value as Procedure;
				diagnose.Call(CurrentObject);
//			} catch (Exception e) {
//				MessageBox.Show("Sorry, I couldn't load the diagnostics system because "+e.Message);
//			}
		}

		public void Explain() {
#if OLD_EXPLAIN
			object ob = CurrentObject;
			string name="";

			if (ob is EvalStack)
				ob = ((InspectorField)Items[0]).fieldValue;

			if (ob is string)
				name = (string)ob;
			else if (ob is Exception)
				name = Installation.HelpFilePath("Exception", ob.GetType().Name);
			else if (ob is Type)
				name = Installation.HelpFilePath("Type", ((Type)ob).Name);
			new HelpBrowser(name).Show();
#else
			
			new System.Threading.Thread(new System.Threading.ThreadStart(ReallyExplain)).Start();
		}
#endif
	}
}